一、概述
Eureka 是 Netflix 开发的服务发现组件,本身是一个基于 REST 的服务。Spring Cloud 将它集成在其子项目 spring-cloud-netflix 中,以实现 Spring Cloud 的服务注册于发现,同时 还提供了负载均衡、故障转移等能力。
1.1 三个角色
| 角色 | 功能 |
|---|---|
| Eureka Server | 通过 Register、Get、Renew 等接口提供服务的注册和发现。 |
| Application Service (Service Provider) | 服务提供方 把自身的服务实例注册到 Eureka Server 中 |
| Application Client (Service Consumer) | 服务调用方 通过 Eureka Server 获取服务列表,消费服务。 |
1.2 高可用环境搭建
Eureka Server 高可用环境需要部署两个Eureka server,它们互相向对方注册。启动不同的端口,如下图:

1.3 自我保护
一般情况下,微服务在 Eureka 上注册后,会每 30 秒发送心跳包,Eureka 通过心跳来 判断服务时候健康,同时会定期删除超过 90 秒没有发送心跳服务。
自我保护:Eureka 设置了一个阀值,当判断挂掉的服务的数量超过阀值时, Eureka Server 认为很大程度上出现了网络故障,将不再删除心跳过期的服务。 阈值是:15 分钟之内是否低于 85%;
1.4 元数据
Eureka的元数据有两种:标准元数据和自定义元数据。
- 标准元数据:主机名、IP地址、端口号、状态页和健康检查等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用。
- 自定义元数据:可以使用eureka.instance.metadata-map配置,符合KEY/VALUE的存储格式。这些元数据可以在远程客户端中访问。
在程序中可以使用DiscoveryClient 获取指定微服务的所有元数据信息
@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class RestTemplateTest {
@Autowired
private DiscoveryClient discoveryClient;
@Test
public void test() {
//根据微服务名称从注册中心获取相关的元数据信息
List<ServiceInstance> instances = discoveryClient.getInstances("shopservice-product");
for (ServiceInstance instance : instances) {
System.out.println(instance);
}
}
}二、使用Demo
2.1 服务端
依赖
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'配置
server:
port: 50101 #服务端口
spring:
application:
name: xc‐govern‐center #指定服务名
eureka:
client:
registerWithEureka: false #服务注册,是否将自己注册到Eureka服务中
fetchRegistry: false #服务发现,是否从Eureka中获取注册信息
serviceUrl: #Eureka客户端与Eureka服务端的交互地址,高可用状态配置对方的地址,单机状态配置自己(如果不配置则默认本机 8761 端口)
defaultZone: http://localhost: 50101 /eureka/
server:
enable‐self‐preservation: false #是否开启自我保护模式
eviction‐interval‐timer‐in‐ms: 60000 #服务注册表清理间隔(单位毫秒,默认是 60 * 1000 )
instance:
preferIpAddress: true #使用IP注册
lease-expiration-duration-in-seconds: 10 #eureka client发送心跳给server端后,续约到期时间(默认90秒)
lease-renewal-interval-in-seconds: 5 #发送心跳续约间
instance-id: ${spring.cloud.client.ip-address}:${server.port} #spring.cloud.client.ip-address:获取ip地址启用
启动类添加@EnableEurekaServer 标识这是一个Eureka服务
2.2 客户端
配置
eureka:
client:
registerWithEureka: true #服务注册开关
fetchRegistry: true #服务发现开关
serviceUrl: #Eureka客户端与Eureka服务端进行交互的地址,多个中间用逗号分隔
defaultZone: ${EUREKA_SERVER:http://localhost:50101/eureka/}
instance:
prefer‐ip‐address: true #将自己的ip地址注册到Eureka服务中
ip‐address: ${IP_ADDRESS:127.0.0.1}
instance‐id: ${spring.application.name}:${server.port} #指定实例id启用
启动类添加注解@EnableDiscoveryClient ,表示它是一个Eureka的客户端
2.3 服务端获取Eureka实例相关信息
import com.commnetsoft.commons.Pager;
import com.commnetsoft.core.CommonError;
import eureka.model.EurekaLogPageQueryDto;
import eureka.model.InstanceInfoDto;
import eureka.model.InstanceQueryDto;
import exception.MicroRuntimeException;
import com.netflix.appinfo.ApplicationInfoManager;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.appinfo.LeaseInfo;
import com.netflix.config.ConfigurationManager;
import com.netflix.discovery.shared.Application;
import com.netflix.discovery.shared.Pair;
import com.netflix.eureka.EurekaServerContext;
import com.netflix.eureka.cluster.PeerEurekaNode;
import com.netflix.eureka.registry.PeerAwareInstanceRegistry;
import com.netflix.eureka.resources.StatusResource;
import com.netflix.eureka.util.StatusInfo;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.net.URI;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* @author wangxp
* @Date 2020/10/21
*/
@Service
public class MonitoringService {
private static final Logger log = LoggerFactory.getLogger(MonitoringService.class);
@Autowired
private ApplicationInfoManager applicationInfoManager;
@Autowired
private PeerAwareInstanceRegistry registry;
@Autowired
private EurekaServerContext eurekaServerContext;
/**
* 获取eureka上注册的服务实例信息
* @Params:[service]
* @Return: java.util.ArrayList
**/
public List<Map<String, Object>> getServiceList( InstanceQueryDto queryDto){
//获取所有实例
List<Application> sortedApplications = registry.getSortedApplications();
ArrayList<Map<String, Object>> maps = new ArrayList<>();
Iterator var4 = sortedApplications.iterator();
while(var4.hasNext()) {
// 获取某名称下的所有实例
Application app = (Application)var4.next();
String appName = app.getName();
String service = queryDto.getService();
// 根据条件查询 名称不同跳过
if (StringUtils.isNotBlank(service) && (!appName.toLowerCase().equals(service.toLowerCase()))){
continue;
}
Map<String, Object> map = new HashMap<>();
map.put("appName",appName);
List<Map<String, Object>> instances = new ArrayList<>();
Iterator var10 = app.getInstances().iterator();
//获取信息
while(var10.hasNext()) {
InstanceInfo info = (InstanceInfo)var10.next();
Map<String, Object> instanceMap = new HashMap<>();
instanceMap.put("instanceId",info.getInstanceId());
instanceMap.put("ipAddr",info.getIPAddr());
instanceMap.put("statu",info.getStatus().name());
instances.add(instanceMap);
}
map.put("instances",instances);
maps.add(map);
}
return maps;
}
/**
* 获取实例集合
* @Params:[appName]
* @Return: java.util.List<java.util.Map<java.lang.String,java.lang.Object>>
**/
public List<Map<String, Object>> getInstanceList(String appName) {
//获取所有实例
List<Application> sortedApplications = registry.getSortedApplications();
List<Map<String, Object>> instances = new ArrayList<>();
Iterator var4 = sortedApplications.iterator();
while(var4.hasNext()) {
Application app = (Application)var4.next();
String name = app.getName();
// 根据条件查询 名称不同跳过
if (!name.toLowerCase().equals(appName.toLowerCase())){
continue;
}
Iterator var10 = app.getInstances().iterator();
//获取信息
while(var10.hasNext()) {
InstanceInfo info = (InstanceInfo)var10.next();
Map<String, Object> instanceMap = new HashMap<>();
instanceMap.put("instanceId",info.getInstanceId());
instanceMap.put("ipAddr",info.getIPAddr());
instanceMap.put("statu",info.getStatus().name());
LeaseInfo leaseInfo = info.getLeaseInfo();
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
instanceMap.put("registrationTime",format.format(new Date(leaseInfo.getRegistrationTimestamp())));
instanceMap.put("renewalTime",format.format(new Date(leaseInfo.getRenewalTimestamp())));
instances.add(instanceMap);
}
break;
}
return instances;
}
/**
* 获取Eureka实例的相关信息
* @Params:[]
* @Return: java.util.Map<java.lang.String,java.lang.Object>
**/
public Map<String, Object> getEurekaStatu(){
Map<String, Object> model = new HashMap<>();
//基本信息
model.put("environment", ConfigurationManager.getDeploymentContext().getDeploymentEnvironment());
model.put("datacenter", ConfigurationManager.getDeploymentContext().getDeploymentDatacenter());
model.put("currentTime", StatusResource.getCurrentTimeAsString());
model.put("upTime", StatusInfo.getUpTime());
model.put("leaseExpirationEnabled",registry.isLeaseExpirationEnabled());
model.put("numOfRenewsPerMinThreshold",registry.getNumOfRenewsPerMinThreshold());
model.put("numOfRenewsInLastMin",registry.getNumOfRenewsInLastMin());
model.put("selfPreservationModeEnabled",registry.isSelfPreservationModeEnabled());
model.put("isBelowRenewThresold", registry.isBelowRenewThresold() == 1);
//状态信息
StatusInfo statusInfo;
try {
statusInfo = (new StatusResource()).getStatusInfo();
} catch (Exception var5) {
statusInfo = StatusInfo.Builder.newBuilder().isHealthy(false).build();
}
model.put("generalStats", statusInfo.getGeneralStats());
model.put("applicationStats",statusInfo.getApplicationStats());
// Eureka集群信息
Map<String, String> replicas = new LinkedHashMap();
List<PeerEurekaNode> list = eurekaServerContext.getPeerEurekaNodes().getPeerNodesView();
Iterator var5 = list.iterator();
while(var5.hasNext()) {
PeerEurekaNode node = (PeerEurekaNode)var5.next();
try {
URI uri = new URI(node.getServiceUrl());
String href = this.scrubBasicAuth(node.getServiceUrl());
replicas.put(uri.getHost(), href);
} catch (Exception var9) {
}
}
model.put("replicas",replicas);
//当前Eureka实例信息
InstanceInfo instanceInfo = statusInfo.getInstanceInfo();
Map<String, String> instanceMap = new HashMap();
instanceMap.put("ipAddr", instanceInfo.getIPAddr());
instanceMap.put("status", instanceInfo.getStatus().toString());
model.put("instanceInfo",instanceMap);
return model;
}
/**
* 获取注销或注册日志 eureka重启后清空
* @Params:[type] 0为 查看取消日志 1为查看注册日志
* @Return: java.util.List
**/
public Pager<Map<String, Object>> getEurekaLog(EurekaLogPageQueryDto pageQueryDto){
ArrayList<Map<String, Object>> logs = new ArrayList();
List<Pair<Long, String>> list;
//类型不为注销日志时,查看注册日志
if (EurekaLogPageQueryDto.LOGOUTSTATU.equals(pageQueryDto.getType())){
list = registry.getLastNCanceledInstances();
}else {
list = registry.getLastNRegisteredInstances();
}
int pagesize = pageQueryDto.getPagesize();
int pagenum = pageQueryDto.getPagenum();
//获取分页后的list集合
List pageList = getPageList(list, pagesize, pagenum);
Iterator var6 = pageList.iterator();
while(var6.hasNext()) {
Pair<Long, String> entry = (Pair)var6.next();
logs.add(this.registeredInstance((String)entry.second(), (Long)entry.first()));
}
Pager<Map<String, Object>> pager = new Pager<>();
pager.setTotal((long)list.size());
pager.setPagenum(pagenum);
pager.setPagesize(pagesize);
pager.setRows(logs);
return pager;
}
/**
* 删除服务(若服务未关闭,则在下次心跳还会再次注册)
* @Params:[appName, instanceId]
* @Return: com.commnetsoft.commons.Result<java.lang.Void>
**/
public void cancelLease(InstanceInfoDto infoDto) throws MicroRuntimeException {
String appName = infoDto.getAppName();
String instanceId = infoDto.getInstanceId();
boolean isSuccess;
try {
isSuccess = registry.cancel(appName, instanceId, false);
} catch (Throwable e) {
log.error("删除失败 (cancel): {} - {}", appName, instanceId, e);
throw new MicroRuntimeException(CommonError.unknown,"删除失败");
}
if (isSuccess) {
log.debug("删除成功: {} - {}", appName, instanceId);
} else {
log.info("未找到 (Cancel): {} - {}", appName, instanceId);
throw new MicroRuntimeException(CommonError.notfound,"未找到实例");
}
}
/**
* 实例状态更新
* @Params:[appName, instanceId, statu] statu为 1 暂停服务,为其他值恢复正常状态
* @Return: com.commnetsoft.commons.Result<java.lang.Void>
**/
public void statusUpdate(InstanceInfoDto infoDto) throws MicroRuntimeException {
String appName = infoDto.getAppName();
String instanceId = infoDto.getInstanceId();
Integer statu = infoDto.getStatu();
//校验状态值
if (null == statu){
throw new MicroRuntimeException(CommonError.illegal_args,"状态值为空");
}
InstanceInfo.InstanceStatus instancestStatu;
if (statu.equals(InstanceInfoDto.PAUSESTATU)){
instancestStatu = InstanceInfo.InstanceStatus.OUT_OF_SERVICE;
}else if (statu.equals(InstanceInfoDto.OPERATIONSTATU)){
instancestStatu = InstanceInfo.InstanceStatus.UP;
}else {
throw new MicroRuntimeException(CommonError.illegal_args,"状态值不合法");
}
//校验实例是否存在
if (registry.getInstanceByAppAndId(appName, instanceId) == null) {
log.warn("未找到实例: {}/{}", appName, instanceId);
throw new MicroRuntimeException(CommonError.notfound,"未找到实例");
}
boolean isSuccess;
try {
//对状态值进行更新
isSuccess = registry.statusUpdate(appName, instanceId,
instancestStatu, null,
false);
} catch (Throwable e) {
log.error("Error updating instance {} for status {}", instanceId,
InstanceInfo.InstanceStatus.OUT_OF_SERVICE,e);
throw new MicroRuntimeException(CommonError.unknown,"更新实例状态失败");
}
if (isSuccess) {
log.info("更新状态成功: {} - {} - {}", appName, instanceId, instancestStatu);
} else {
log.warn("更新失败失败: {} - {} - {}", appName, instanceId, instancestStatu);
throw new MicroRuntimeException(CommonError.unknown,"更新实例状态失败");
}
}
/**
* 清理配置中Eureka链接上的身份验证信息
* @Params:[urlList]
* @Return: java.lang.String
**/
private String scrubBasicAuth(String urlList) {
String[] urls = urlList.split(",");
StringBuilder filteredUrls = new StringBuilder();
String[] var4 = urls;
int var5 = urls.length;
for(int var6 = 0; var6 < var5; ++var6) {
String u = var4[var6];
if (u.contains("@")) {
filteredUrls.append(u, 0, u.indexOf("//") + 2).append(u.substring(u.indexOf("@") + 1)).append(",");
} else {
filteredUrls.append(u).append(",");
}
}
return filteredUrls.substring(0, filteredUrls.length() - 1);
}
/**
* 注册实例 id及时间
* @Params:[id, date]
* @Return: java.util.Map<java.lang.String,java.lang.Object>
**/
private Map<String, Object> registeredInstance(String id, long date) {
HashMap<String, Object> map = new HashMap();
map.put("id", id);
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
map.put("date", format.format(new Date(date)));
return map;
}
/**
* 分页方法
* @Return: java.util.List
**/
private List getPageList(List list, Integer pagesize,Integer pagenum){
int size = list.size();
//集合为空直接返回
if (size == 0){
return list;
}
// size大于需要数量时直接返回
if (size >= pagesize*pagenum ){
if (pagenum ==1){
return list.subList(0,pagesize);
}else {
return list.subList((pagenum-1)*pagesize,pagesize*pagenum);
}
}else{
if(pagenum == 1){
return list.subList(0,size);
}else{
int i = size/pagesize;
int j = size%pagesize;
if (j != 0){
if (i ==1){
return list.subList(pagesize,size);
}else{
return list.subList(i*pagesize,size);
}
}else{
if (i ==1){
return list.subList(0,size);
}else{
return list.subList((i-1)*pagesize,size);
}
}
}
}
}
}2.4 安全注册认证
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>配置
#开启 http basic 的安全认证
security.basic.enabled=true
security.user.name=user
security.user.password=123456服务注册
#设置服务注册中心地址,指向另一个注册中心
eureka.client.serviceUrl.defaultZone=http://user:123456@eureka1:8761/eureka/,http://user:123456@eureka2:8761/eureka/三、常见问题
3.1 服务注册慢
服务的注册涉及到心跳,默认心跳间隔为30s。在实例、服务器、客户端都在本地缓存中具有相同的元数据之前,服务不可用于客户端发现(所以可能需要3次心跳)。可以通过配置eureka.instance.leaseRenewalIntervalInSeconds (心跳频率)加快客户端连接到其他服务的过程。在生产中,最好坚持使用默认值,因为在服务器内部有一些计算,他们对续约做出假设。
3.2 服务节点剔除
默认情况下,由于Eureka Server剔除失效服务间隔时间为90s且存在自我保护的机制。所以不能有效而迅速的剔除失效节点,这对开发或测试会造成困扰。解决方案如下:
Eureka Server:配置关闭自我保护,设置剔除无效节点的时间间隔
Eureka Client:配置开启健康检查,并设置续约时间